/*
 * Copyright (c) 2013 Mobs & Geeks
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the
 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.mobsandgeeks.adapters;

import ohos.aafwk.ability.Ability;
import ohos.agp.components.BaseItemProvider;
import ohos.agp.components.Component;
import ohos.agp.components.ComponentContainer;
import ohos.agp.components.LayoutScatter;
import ohos.agp.components.Text;
import ohos.app.Context;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;

/**
 * 通过将模型中的Instant
 * 注释方法映射到布局上的 {@link Component} 来构建自定义 {@link BaseItemProvider} 的类。可以使用 {@link InstantText} 注释来注释方法
 *
 * @param <T> T
 * @since 2021-07-01
 */
public class InstantAdapterCore<T> {
    private static final String EMPTY_STRING = "";
    private static final int NUMF_1 = -1;
    private Ability mContext;
    private int mLayoutResourceId;
    private LayoutScatter mLayoutInflater;
    private Class<?> mDataType;
    private HashMap<Integer, Meta> mViewIdsAndMetaCache;
    private HashMap<Integer, SimpleDateFormat> mDateFormatCache;
    private HashMap<Integer, String> mFormatStringCache;

    /**
     * Constructs
     *
     * @param context The {@link Context} to use.
     * @param adapter The adapter using this instance of {@link InstantAdapterCore}.
     * @param layoutResourceId The resource id of your XML layout.
     * @param dataType The data type backed by your adapter.
     * @throws IllegalArgumentException If {@code context} is null or {@code layoutResourceId} is
     * invalid or {@code type} is {@code null}.
     */
    public InstantAdapterCore(final Ability context, final BaseItemProvider adapter,
                              final int layoutResourceId, final Class<?> dataType) {
        if (context == null) {
            throw new IllegalArgumentException("'context' cannot be null.");
        } else if (layoutResourceId == 0) {
            throw new IllegalArgumentException("Invalid 'layoutResourceId', please check again.");
        } else if (dataType == null) {
            throw new IllegalArgumentException("'dataType' cannot be null.");
        }

        mContext = context;
        mLayoutResourceId = layoutResourceId;
        mLayoutInflater = LayoutScatter.getInstance(context);
        mDataType = dataType;
        mViewIdsAndMetaCache = new HashMap<Integer, Meta>();
        mDateFormatCache = new HashMap<Integer, SimpleDateFormat>();
        mFormatStringCache = new HashMap<Integer, String>();
        findAnnotatedMethods();
    }

    /**
     * 设置数据
     */
    private void findAnnotatedMethods() {
        Class<?> clazz = mDataType;
        do {
            findAnnotatedMethods(clazz);
            clazz = clazz.getSuperclass();
        } while (!clazz.equals(Object.class));
    }

    private void findAnnotatedMethods(Class<?> clazz) {
        Method[] declaredMethods = clazz.getDeclaredMethods();
        for (Method method : declaredMethods) {
            Annotation[] annotations = method.getAnnotations();
            for (Annotation annotation : annotations) {
                if (isInstantAnnotation(annotation)) {
                    // Assertions
                    assertMethodIsPublic(method);
                    assertNoParamsOrSingleContextParam(method);
                    assertNonVoidReturnType(method);
                    Meta meta = new Meta(annotation, method);
                    if (annotation instanceof InstantText) {
                        mViewIdsAndMetaCache.put(((InstantText) annotation).viewId(), meta);
                    }
                }
            }
        }
    }

    /**
     * 是否为InstantAnnotation注解
     *
     * @param annotation annotation
     * @return 是否为InstantAnnotation注解
     */
    private boolean isInstantAnnotation(final Annotation annotation) {
        return annotation.annotationType().equals(InstantText.class);
    }

    /**
     * 是否为公共的方法
     *
     * @param method Method
     */
    private void assertMethodIsPublic(final Method method) {
        if (!Modifier.isPublic(method.getModifiers())) {
            throw new IllegalStateException(String.format("%s.%s() should be public",
                    mDataType.getSimpleName(), method.getName()));
        }
    }

    /**
     * 调用反射方法
     *
     * @param method Method
     * @param instance T
     * @return Object
     */
    private Object invokeReflectedMethod(final Method method, final T instance) {
        Object returnValue = null;
        try {
            Class<?>[] parameterTypes = method.getParameterTypes();
            int nparameters = parameterTypes.length;
            if (nparameters == 0) {
                returnValue = method.invoke(instance);
            } else if (nparameters == 1) {
                returnValue = method.invoke(instance, mContext);
            }
        } catch (IllegalArgumentException e) {
            LogUtils.log(LogUtils.DEBUG, "Exception", e.getMessage());
        } catch (IllegalAccessException e) {
            LogUtils.log(LogUtils.DEBUG, "Exception", e.getMessage());
        } catch (InvocationTargetException e) {
            LogUtils.log(LogUtils.DEBUG, "InvocationTargetException", e.getMessage());
        }
        return returnValue;
    }

    /**
     * NoParams或SingleContextParam
     *
     * @param method Method
     * @throws IllegalStateException
     */
    private void assertNoParamsOrSingleContextParam(final Method method) {
        Class<?>[] parameters = method.getParameterTypes();
        final int nparameters = parameters.length;
        if (nparameters > 0) {
            String errorMessage = String.format("%s.%s() can have an single Context "
                    + "parameter or should have no parameters.",mDataType.getSimpleName(), method.getName());
            if (parameters.length == 1) {
                Class<?> parameterType = parameters[0];
                if (!parameterType.isAssignableFrom(Context.class)) {
                    throw new IllegalStateException(errorMessage);
                }
            } else if (parameters.length > 1) {
                throw new IllegalStateException(errorMessage);
            }
        }
    }

    /**
     * 非空返回类型
     *
     * @param method Method
     */
    private void assertNonVoidReturnType(final Method method) {
        if (method.getReturnType().equals(Void.TYPE)) {
            throw new UnsupportedOperationException(
                    String.format("Methods with void return types cannot be annotated, "
                            +
                            "check %s.%s()", mDataType.getSimpleName(), method.getName()));
        }
    }

    /**
     * 创建item
     *
     * @param componentContainer ComponentContainer
     * @return Component
     */
    public Component createNewView(ComponentContainer componentContainer) {
        Component view = mLayoutInflater.parse(mLayoutResourceId, componentContainer, false);
        HashMap<Integer, Holder> holders = new HashMap<Integer, Holder>();
        int size = mViewIdsAndMetaCache.size();
        for (int ii = 0; ii < size; ii++) {
            List<Integer> list = new ArrayList<>(mViewIdsAndMetaCache.keySet());
            int viewId = list.get(ii);
            Meta meta = mViewIdsAndMetaCache.get(viewId);
            Component viewFromLayout = view.findComponentById(viewId);
            if (viewFromLayout == null) {
                String message = String.format("Cannot find View, check the 'viewId' "
                                +
                                "attribute on method %s.%s()",
                        mDataType.getName(), meta.method.getName());
                LogUtils.log(LogUtils.DEBUG, "InvocationTargetException",message);
            } else {
                holders.put(viewId, new Holder(viewFromLayout, meta));
            }
        }
        view.setTag(holders);
        return view;
    }

    /**
     * 绑定数据
     *
     * @param componentContainer ComponentContainer
     * @param cpt Component
     * @param instance T
     * @param position position
     */
    public void bindToView(ComponentContainer componentContainer, Component cpt, T instance, int position) {
        HashMap<Integer, Holder> holders = (HashMap<Integer, Holder>) cpt.getTag();
        updateAnnotatedViews(holders, cpt, instance, position);
    }

    /**
     * updateAnnotatedViews
     *
     * @param holders holders
     * @param cpt Component
     * @param instance T
     * @param position int
     */
    private void updateAnnotatedViews(HashMap<Integer, Holder> holders,
                                      final Component cpt, final T instance, final int position) {
        int nholders = holders.size();
        for (int ii = 0; ii < nholders; ii++) {
            List<Holder> list = new ArrayList<>(holders.values());
            Holder holder = list.get(ii);
            Meta meta = holder.meta;
            if (meta == null) {
                continue;
            }
            Object returnValue = invokeReflectedMethod(meta.method, instance);
            Class<? extends Component> viewType = holder.view.getClass();
            if (Text.class.isAssignableFrom(viewType)) {
                InstantText instantText = (InstantText) meta.annotation;
                Text textView = (Text) holder.view;
                int viewId = textView.getId();
                String text = "null";
                if (returnValue != null) {
                    text = applyDatePattern(viewId, instantText, returnValue);
                    text = applyFormatString(viewId, instantText, text, returnValue);
                    if (text == null) {
                        text = returnValue.toString();
                    }
                }
                textView.setText(text);
            }
        }
    }

    /**
     * applyDatePattern
     *
     * @param viewId
     * @param instantText
     * @param returnValue
     * @return String
     */
    private String applyDatePattern(final int viewId, final InstantText instantText,
                                    final Object returnValue) {
        int index = NUMF_1;
        if (mDateFormatCache.size() != 0) {
            List<Integer> list = new ArrayList<>(mDateFormatCache.keySet());
            index = list.indexOf(viewId);
        }
        SimpleDateFormat simpleDateFormat = null;
        String text = null;

        if (index > NUMF_1) {
            simpleDateFormat = mDateFormatCache.get(viewId);
        } else {
            int datePatternRes = instantText.datePatternResId();
            String datePattern = datePatternRes != 0
                    ?
                    mContext.getString(datePatternRes) : instantText.datePattern();

            if (datePattern != null && !EMPTY_STRING.equals(datePattern)) {
                simpleDateFormat = new SimpleDateFormat(datePattern, Locale.getDefault());
                mDateFormatCache.put(viewId, simpleDateFormat);
            }
        }

        if (simpleDateFormat != null) {
            text = simpleDateFormat.format(returnValue);
        }

        return text;
    }

    /**
     * applyFormatString
     *
     * @param viewId viewId
     * @param instantText instantText
     * @param dateFormattedString String
     * @param returnValue Object
     * @return String
     */
    private String applyFormatString(final int viewId, final InstantText instantText,
                                     final String dateFormattedString, final Object returnValue) {
        int index = NUMF_1;
        if (mFormatStringCache != null && mFormatStringCache.size() != 0) {
            List<Integer> list = new ArrayList<>(mFormatStringCache.keySet());
            index = list.indexOf(viewId);
        }
        String formatString = null;
        String formatted = dateFormattedString;
        if (index > NUMF_1) {
            formatString = mFormatStringCache.get(viewId);
        } else {
            int formatStringRes = instantText.formatStringResId();
            formatString = formatStringRes != 0
                    ? mContext.getString(formatStringRes) : instantText.formatString();
            mFormatStringCache.put(viewId, formatString);
        }

        if (formatString != null && !EMPTY_STRING.equals(formatString)) {
            formatted = String.format(formatString, dateFormattedString != null
                    ? dateFormattedString : returnValue);
        }
        return formatted;
    }

    /**
     * Meta
     *
     * @since 2021-07-01
     */
    private static class Meta {
        Annotation annotation;
        Method method;

        Meta(final Annotation annotation, final Method method) {
            this.annotation = annotation;
            this.method = method;
        }
    }

    /**
     * Holder
     *
     * @since 2021-07-02
     */
    private static class Holder {
        Component view;
        Meta meta;

        Holder(final Component view, final Meta meta) {
            this.view = view;
            this.meta = meta;
        }
    }
}
